a tool for shared writing and social publishing
at update/thread-viewer 142 lines 4.9 kB view raw
1import { subscribeToPublication } from "app/lish/subscribeToPublication"; 2import { cookies } from "next/headers"; 3import { redirect } from "next/navigation"; 4import { NextRequest, NextResponse } from "next/server"; 5import { createOauthClient } from "src/atproto-oauth"; 6import { setAuthToken } from "src/auth"; 7 8import { supabaseServerClient } from "supabase/serverClient"; 9import { URLSearchParams } from "url"; 10import { 11 ActionAfterSignIn, 12 parseActionFromSearchParam, 13} from "./afterSignInActions"; 14import { inngest } from "app/api/inngest/client"; 15 16type OauthRequestClientState = { 17 redirect: string | null; 18 action: ActionAfterSignIn | null; 19}; 20 21export async function GET( 22 req: NextRequest, 23 props: { params: Promise<{ route: string; handle?: string }> }, 24) { 25 const params = await props.params; 26 let client = await createOauthClient(); 27 switch (params.route) { 28 case "metadata": 29 return NextResponse.json(client.clientMetadata); 30 case "jwks": 31 return NextResponse.json(client.jwks); 32 case "login": { 33 const searchParams = req.nextUrl.searchParams; 34 const handle = searchParams.get("handle") as string; 35 // Put originating page here! 36 let redirect = searchParams.get("redirect_url"); 37 if (redirect) redirect = decodeURIComponent(redirect); 38 let action = parseActionFromSearchParam(searchParams.get("action")); 39 let state: OauthRequestClientState = { redirect, action }; 40 41 // Revoke any pending authentication requests if the connection is closed (optional) 42 const ac = new AbortController(); 43 44 const url = await client.authorize(handle || "https://bsky.social", { 45 scope: "atproto transition:generic transition:email", 46 signal: ac.signal, 47 state: JSON.stringify(state), 48 }); 49 50 return NextResponse.redirect(url); 51 } 52 case "callback": { 53 const params = new URLSearchParams(req.url.split("?")[1]); 54 55 let redirectPath = "/"; 56 try { 57 const { session, state } = await client.callback(params); 58 let s: OauthRequestClientState = JSON.parse(state || "{}"); 59 redirectPath = decodeURIComponent(s.redirect || "/"); 60 let { data: identity } = await supabaseServerClient 61 .from("identities") 62 .select() 63 .eq("atp_did", session.did) 64 .single(); 65 if (!identity) { 66 let existingIdentity = (await cookies()).get("auth_token"); 67 if (existingIdentity) { 68 let data = await supabaseServerClient 69 .from("email_auth_tokens") 70 .select("*, identities(*)") 71 .eq("id", existingIdentity.value) 72 .single(); 73 if (data.data?.identity && data.data.confirmed) 74 await supabaseServerClient 75 .from("identities") 76 .update({ atp_did: session.did }) 77 .eq("id", data.data.identity); 78 79 return handleAction(s.action, redirectPath); 80 } 81 const { data } = await supabaseServerClient 82 .from("identities") 83 .insert({ atp_did: session.did }) 84 .select() 85 .single(); 86 identity = data; 87 } 88 89 // Trigger migration if identity needs it 90 const metadata = identity?.metadata as Record<string, unknown> | null; 91 if (metadata?.needsStandardSiteMigration) { 92 await inngest.send({ 93 name: "user/migrate-to-standard", 94 data: { did: session.did }, 95 }); 96 } 97 98 let { data: token } = await supabaseServerClient 99 .from("email_auth_tokens") 100 .insert({ 101 identity: identity!.id, 102 confirmed: true, 103 confirmation_code: "", 104 }) 105 .select() 106 .single(); 107 108 if (token) await setAuthToken(token.id); 109 110 // Process successful authentication here 111 console.log("authorize() was called with state:", state); 112 113 console.log("User authenticated as:", session.did); 114 return handleAction(s.action, redirectPath); 115 } catch (e) { 116 redirect(redirectPath); 117 } 118 } 119 default: 120 return NextResponse.json({ error: "Invalid route" }, { status: 404 }); 121 } 122} 123 124const handleAction = async ( 125 action: ActionAfterSignIn | null, 126 redirectPath: string, 127) => { 128 let parsePath = decodeURIComponent(redirectPath); 129 let url; 130 if (parsePath.includes("://")) url = new URL(parsePath); 131 else url = new URL(decodeURIComponent(redirectPath), "https://example.com"); 132 if (action?.action === "subscribe") { 133 let result = await subscribeToPublication(action.publication); 134 if (result.success && result.hasFeed === false) 135 url.searchParams.set("showSubscribeSuccess", "true"); 136 } 137 138 let path = url.pathname; 139 if (url.search) path += url.search; 140 if (url.hash) path += url.hash; 141 return parsePath.includes("://") ? redirect(url.toString()) : redirect(path); 142};